TailsNS.HTML = {
  isTag: function(n, name) {
    return n.nodeName && n.nodeName.toLowerCase() == name;
  },
  isTagWithAttr: function(n, tag, attr) {
    return this.isTag(n,tag) && n.getAttribute(attr);
  },
  isText: function(n) {
    return n.nodeName == "#text"
  },
  
  classRE: function(cls) { return new RegExp("(?:\\s|^)" + cls + "(?:\\s|$)"); },
  isClass: function(n, cls) {
    if (!this.isText(n) && n.nodeName != "#comment") {
      if (n.getAttribute("class")) {
        return n.getAttribute("class").match(this.classRE(cls));
      }
    }
  },
  isAnyClass: function(n, arr) {
    for (var i=0; i<arr.length; i++) {
      if (this.isClass(n,arr[i])) return true;
    }
    return false;
  },
  isAttr: function(n, attrName, value) {
    return n.getAttribute(attrName) == value;
  },
  
  // retrieve the attribute of a child element
  cAttr: function(parent, childTag, attrName) {
    var e = parent.getElementsByTagName(childTag);
    return (e && e.length > 0) ? e[0].getAttribute(attrName) : null;
  }
}

var FormatBase = {
  daysOfWeek: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
  months: ["January", "February", "March", "April", "May", "June", "July", "August", "September",
      "October", "November", "December"],
  html: TailsNS.HTML,

  createFormat: function(format) {
    format.topNodePattern = this.html.classRE(format.topNodeClass);
    format.html = TailsNS.HTML;
    
    for (m in this.parserMethods)
      format[m] = this.parserMethods[m];

    var classArrays = [
        format.simpleValueClasses,
        format.pluralValueClasses,
        format.urlValueClasses,
        format.pluralURLValueClasses,
        format.emailValueClasses
    ];
    for (var i=0; i<classArrays.length; i++) {
      if (classArrays[i])
        classArrays[i].contains = this.__contains
    }

    return format;
  },

  __contains: function(v) {
    for (var i=0; i<this.length; i++) {
      if (this[i] == v) return true;
    }
    return false;
  },

  toURL: function(doc,v) {
    if (!v) {
      // derive parameters from <a> or <area>
      if (this.html.isTag(doc, "a") || this.html.isTag(doc, "area")) {
        v = doc.getAttribute("href");
        doc = doc.ownerDocument;

      // derive parameters from <img>
      } else if (this.html.isTag(doc, "img")) {
        v = doc.getAttribute("src");
        doc = doc.ownerDocument;
      
      // derive parameters from <object>
      } else if (this.html.isTag(doc, "object")) {
        v = doc.getAttribute("data");
        doc = doc.ownerDocument;
      }
    }
    if (v) {
      if (v.match(/^(.*?):/) && (
          RegExp.$1 == "http"  ||
          RegExp.$1 == "https" ||
          RegExp.$1 == "data"  ||
          RegExp.$1 == "ftp"   ||
          RegExp.$1 == "mailto"))
        return v;

      var href = doc.location.href;
      
      // check for the base tag
      var base = this.html.cAttr(doc, "base", "href")
      
      // check for the xml:base tag
      if (!base) base = this.html.cAttr(doc, "html", "xml:base");
      if (!base) base = this.html.cAttr(doc, "body", "xml:base");
      if (base) href = base;

      if (v.charAt(0) == '/') {
        href.match(/([A-z]+:\/\/[^\/]+)(?:\/.*)?/);
      } else {
        href.match(/(.*\/)[^\/]*/);
      }
      return RegExp.$1 + v;
    }
    return v;
  },
  toDate: function(node) {
    var s = this.toText(node);
    return this.evalDate(s);
  },
  evalDate: function(s) {
    // yyyy-MM-ddTHH:mm
    if (s.match(/(\d{4})-(\d\d?)-(\d\d?)(?:T(\d\d?):(\d\d?))?/)) {
      dt = new Date();
      dt.setFullYear(RegExp.$1, Number(RegExp.$2)-1, RegExp.$3);
      if (RegExp.$4) {
        dt.setHours(RegExp.$4, RegExp.$5, 0, 0);
      } else {
        dt.setHours(0, 0, 0, 0);
      }
      return dt;
    
    // yyyyMMddTHHmmssZ
    } else if (s.match(/(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})?(Z|(?:[+-]\d{4}))?)?/)) {
      dt = new Date();
      dt.setFullYear(RegExp.$1, Number(RegExp.$2)-1, RegExp.$3);
      
      // check for time values.
      if (RegExp.$4) {
        dt.setHours(RegExp.$4, RegExp.$5, 0, 0);
        
        // check for seconds
        if (RegExp.$6)
          dt.setSeconds(RegExp.$6);
        
        // check for time zone
        if (RegExp.$7) {
          var hours = dt.getHours();
          var mins = dt.getMinutes();
          var secs = dt.getSeconds();
          dt.setUTCHours(hours, mins, secs, 0);
          
          var tz = RegExp.$7;
          if (tz != "Z") {
            var dtOffsetHours =  -1 * Number(tz.substring(0, 3));
            dt.setTime(dt.getTime() + (dtOffsetHours * 60 * 60 * 1000));
          }
        }
        
      } else {
        dt.setHours(0, 0, 0, 0);
      }
      return dt;
    
    // HH:mm
    } else if (s.match(/(\d\d?):(\d\d?)/)) {
      dt = new Date();
      dt.setFullYear(0,0,0);
      dt.setHours(RegExp.$1, RegExp.$2, 0, 0);
      return dt;
    }
    return null;
  },
  commaJoin: function(s1,s2) {
    var ret = "";
    if (s1 && s1.length > 0) ret += s1;
    if (s2 && s2.length > 0) {
      if (ret.length > 0) ret += ", ";
      ret += s2;
    }
    return ret;
  },
  toTag: function(node) {
    if (this.html.isTag(node, 'a') && this.html.isAttr(node, "rel", "tag")) {
      var href = node.getAttribute("href");
      if (href.match(/.*?\/([^\/]+)\/?$/))
        return RegExp.$1;
      else
        return href;      
    }
    return null;
  },
  toText: function(node) {
    var v = null;
    if (this.html.isTagWithAttr(node, 'abbr', "title"))
      v = node.getAttribute("title");

    else if (this.html.isTagWithAttr(node, 'img', 'alt'))
      v = node.getAttribute("alt");

    else if ((this.html.isTag(node, 'area') || this.html.isTag(node, 'img')) && node.getAttribute('alt'))
      v = node.getAttribute("alt");
    
    else
    	v = this.trim(this.evalTextNode(node));
    
    if (node.getAttribute("xml:lang")) {
      v.__lang = node.getAttribute("xml:lang");
    }
    
    return v;
  },
  trim: function(s) {
    if (!s) return s;
    if (s.match(/^\s*(.*?)\s*$/)) return RegExp.$1;
    return s;
  },
  evalTextNode: function(node) {
    var html = this.html;
    var text = [];
    
    var loadText = function(n, isValue) {
      if (html.isText(n)) {
        text.push({ text: n.data, value: isValue});
      /*} else if (html.isTag(n, 'abbr') && n.getAttribute('title')) {
        text.push({ text: n.getAttribute("title"),
            value: isValue || html.isClass(n, 'value')});
        return;
      } else if ((html.isTag(n, 'area') || html.isTag(n, 'img')) && n.getAttribute('alt')) {
        text.push({ text: n.getAttribute("alt"),
            value: isValue || html.isClass(n, 'value')});
        return;*/
      }

      if (n.hasChildNodes) {
        isValue = isValue || html.isClass(n, 'value');
        var children = n.childNodes;
        for (var i=0; i<children.length; i++) {
          loadText(children[i], isValue);
        }
      }
    }
    
    loadText(node, html.isClass(node, 'value'));
    
    var values = [];
    var hasValuedNodes = false;
    for (var i=0; i<text.length; i++) {
      if (text[i].value) values.push(text[i].text)
    }
    if (values.length == 0) {
      for (var i=0; i<text.length; i++) {
        values.push(text[i].text);
      }
    }
    return values.join("").replace(/\s+/g, ' ');
  },

  prettyDate: function(dt, format) {
    if (typeof(format) == 'undefined') format = '%A, %B %d, %Y';
    
    if (dt.constructor != Array) {
      return this.prettyDate([
          dt.getFullYear()  // 0
          , dt.getMonth()+1 // 1
          , dt.getDate()    // 2
          , dt.getDay()     // 3
          , dt.getHours()   // 4
          , dt.getMinutes() // 5
          , dt.getSeconds() // 6
        ], format);
    
    } else {
      var pad = function(i) { return ((i < 10 )? "0" : "") + i; }
      return format
          .replace(/%A/g, this.daysOfWeek[dt[3]])
          .replace(/%B/g, this.months[dt[1]-1])
          .replace(/%m/g, pad(dt[1]))
          .replace(/%d/g, pad(dt[2]))
          .replace(/%Y/g, dt[0])
          .replace(/%H/g, pad(dt[4]))
          .replace(/%M/g, pad(dt[5]))
          .replace(/%S/g, pad(dt[6]));
    }
  },
  
  prettyUTCDate: function(dt, format) {
    return this.prettyDate([
        dt.getUTCFullYear()  // 0
        , dt.getUTCMonth()+1 // 1
        , dt.getUTCDate()    // 2
        , dt.getUTCDay()     // 3
        , dt.getUTCHours()   // 4
        , dt.getUTCMinutes() // 5
        , dt.getUTCSeconds() // 6
      ], format);
  },
  
  findNodeForFormat: function(format, node) {
    if (node.nodeName[0] == '#') return null;
    var c = node.getAttribute("class");
    if (c && c.match(format.topNodePattern))
      return node;
    for (var i=0; i<node.childNodes.length; i++) {
      if (node.childNodes[i].hasChildNodes) {
        var r = this.findNodeForFormat(format, node.childNodes[i]);
        if (r) return r;
      }
    }
    return null;
  },

  pluralize: function(s) {
    var c = s[s.length - 1];
    if (c == "x"[0] || c == "h"[0] || c == "s"[0]) {
      return s + "es";
    
    } else {
      return s + "s";
    }
  },
  
  // a helper method for handling plural values.
  _checkPluralValues: function(object, name, value) {
    if (!object[name])
      object[name] = value;
    var pluralizedName = this.pluralize(name);
    if (object[pluralizedName] == null)
      object[pluralizedName] = [];
    object[pluralizedName].push(value);
  },

  // parsing handling methods
  parserMethods: {
    // parse for an object, starting at the given node.
    parseObject: function(node, object) {
      if (!node) alert("parseObject: node is null");
      if (!object) {
        var lang = this.html.cAttr(node.ownerDocument, "html", "xml:lang");
        if (!lang) lang = this.html.cAttr(node.ownerDocument, "html", "lang");
        object = {
          __name: this.name,
          source: {
            url: node.ownerDocument.location.href,
            title: node.ownerDocument.title,
            lang: lang
          }
        }
      }
      
      this._receiveNode(node, object);
      this._parseChildren(node, object);
      this._checkHeaders(node, object);
      this._checkDateValues(object);
      this._addMethods(object);
      this._checkGenericValues(object);

      if (this._parseFinished) this._parseFinished(node, object);
      return object;
    },

    // parse the child nodes
    _parseChildren: function(parent, object) {
      var children = parent.childNodes;
      for (var i=0; i<children.length; i++) {
        var child = children.item(i);
        if (child.hasChildNodes && child.tagName) {
          /* handle the include pattern here */
          if (this.html.isClass(child, 'include')) {
            var ref = null;
            if (this.html.isTag(child, 'object')) {
              ref = child.getAttribute("data");
    
            } else if (this.html.isTag(child, 'a')) {
              ref = child.getAttribute("href");
            }
            if (ref[0] == '#') {
              var include = child.ownerDocument.getElementById(ref.substring(1));
              if (include == null)
                alert("include not found: " + ref.substring(1));
              else
                child = include;
            }
          }
          if (!this.html.isTag(child, "del")) {
            this._receiveNode(child, object);
            this._parseChildren(child, object);
          }
        }
      }
    },
  
    // parse a particular node. 
    _receiveNode: function(node, object) {
      if (node.nodeName == "#text"
          && node.nodeName == "#comment") return;

      var nodeClass = node.getAttribute("class");
      if (!nodeClass) {
        if (this._checkNode) this._checkNode(node, object);
        return;
      }
  
      var styleClasses = nodeClass.split(/\s+/);
      for (var i=0; i<styleClasses.length; i++) {
        if (this._checkCustomBindings(styleClasses[i], node, object)) {
          continue;
        }
  
        this._checkURLValues(styleClasses[i], node, object);
        this._checkPluralURLValues(styleClasses[i], node, object);
        this._checkEmailValues(styleClasses[i], node, object);
        this._checkSimpleValues(styleClasses[i], node, object);
        this._checkPluralValues(styleClasses[i], node, object);
        this._checkEmbeddedFormats(styleClasses[i], node, object);
        if (this._checkNode) this._checkNode(node, object);
      }
    },
    
    // check for table cell header values
    _checkHeaders: function(node, object) {
      if (this.html.isTag(node, 'td') && node.getAttribute("headers")) {
        var headers = node.getAttribute("headers").split(" ");
        for (var i=0; i<headers.length; i++) {
          var n = node.ownerDocument.getElementById(headers[i]);
          if (n) {
            this._receiveNode(n, object);
            this._parseChildren(n, object);
          }
        }
      }
    },
    
    // parse simple text values
    _checkSimpleValues: function(styleClass, node, object) {
      if (this.simpleValueClasses
          && this.simpleValueClasses.contains(styleClass)
          && !object[styleClass]) {
        object[styleClass] = FormatBase.toText(node);
      }
    },
    
    // parse plural text values
    _checkPluralValues: function(styleClass, node, object) {
      if (this.pluralValueClasses
          && this.pluralValueClasses.contains(styleClass)) {

        FormatBase._checkPluralValues(object, styleClass, FormatBase.toText(node));
      }
    },

    // parse url values
    _checkURLValues: function(styleClass, node, object) {
      if (this.urlValueClasses
          && this.urlValueClasses.contains(styleClass)
          && !object[styleClass]) {
        object[styleClass] = FormatBase.toURL(node);
      }
    },
    
    // parse plural url values
    _checkPluralURLValues: function(styleClass, node, object) {
      if (this.pluralURLValueClasses
          && this.pluralURLValueClasses.contains(styleClass)) {

        FormatBase._checkPluralValues(object, styleClass, FormatBase.toURL(node));
      }
    },
    
    // parse email values
    _checkEmailValues: function(styleClass, node, object) {
      if (this.emailValueClasses && this.emailValueClasses.contains(styleClass)
          && !object[styleClass]) {
      
        var email = node.getAttribute("href");
        if (!email) email = FormatBase.toText(node);
        if (email.match(/mailto:([^\?]+)/))
          email = RegExp.$1;
        object[styleClass] = email;
      }
    },
    
    // parse embedded format objects
    _checkEmbeddedFormats: function(styleClass, node, object) {
      if (this.embeddedFormatClasses && this.embeddedFormatClasses[styleClass]) {
        var formatName = this.embeddedFormatClasses[styleClass];
        var format = TailsConfig.getFormat(formatName);
        if (!this.html.isClass(node, format.topNodeClass)) return false;
        if (object[styleClass])
          object[styleClass + "_" + formatName] = format.parseObject(node);
        else
          object[styleClass] = format.parseObject(node);
      }
    },
    
    // parse date values
    _checkDateValues: function(object) {
      if (this.dateValueClasses) {
        for (var i=0; i<this.dateValueClasses.length; i++) {
          if (object[this.dateValueClasses[i]]) {
            object[this.dateValueClasses[i]+"_date"] = FormatBase.evalDate(object[this.dateValueClasses[i]]);
            //alert(this.dateValueClasses[i]+"_date: " + object[this.dateValueClasses[i]+"_date"]);
          }
        }
      }
    },
    
    // determine the generic values for this object.
    _checkGenericValues: function(object) {
      if (!object["generic"]) object["generic"] = {};
  
      var genericCheck = function(format, name) {
        var objectKey = name.toLowerCase();
        var getMethod = "_get"+name;
        var attrs = format["generic"+name+"Attributes"];
  
        if (format[getMethod]) object["generic"][objectKey] = format[getMethod](object);
        if (attrs && !object["generic"][objectKey]) {
          object["generic"][objectKey] = object[attrs[0]];
          for (var i=1; i<attrs.length && !object["generic"][objectKey]; i++) {
            object["generic"][objectKey] = object[attrs[i]];
          }
        }
      };
  
      genericCheck(this, "Label");
      genericCheck(this, "URL");
      genericCheck(this, "Icon");
      genericCheck(this, "Description");
    },
    
    // perform any custom parsing functions.
    _checkCustomBindings: function(styleClass, node, object) {
      if (this.customBindings) {
        this.customBindings.html = this.html;
        for (var k in this.customBindings) {
          if (k == styleClass) {
            return this.customBindings[k](node, object);
          }
        }
      }
      return false;
    },
    
    // add methods to the parsed object.
    _addMethods: function(object) {
      if (this.objectMethods) {
        for (var m in this.objectMethods) {
          //alert(m);
          object[m] = this.objectMethods[m];
        }
      }
    },
  },

  end: null
}
